﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;

using ThingyCloudWPF.Creatures;
using ThingyCloudWPF.Network;
using ThingyCloudWPF.Network.Messages;

namespace ThingyCloudWPF.World
{
	/// <summary>
	/// This represents the actual scene of the ceiling. It will receive where blobs are and can realize where
	/// we should merge them to multiple avatars or just create a single avatar from it.
	/// </summary>
	public class Scene
	{
		/// <summary>
		/// Whether we should stop living or not
		/// </summary>
		private bool stop = false;

		/// <summary>
		/// The nature thread, makes sure that thingies are born and killed. Balances the eco-system.
		/// </summary>
		private Thread nature;
		
		/// <summary>
		/// A list of all the blobs that are on the scene
		/// </summary>
		private List<Blob> Blobs { get; set; }

		/// <summary>
		/// The current avatars that are on the scene
		/// </summary>
		private List<Avatar> Avatars { get; set; }

		/// <summary>
		/// All thingies that are currently on scene
		/// </summary>
		private List<Thingy> Thingies { get; set; }

		private Object sceneLock = new Object();

		/// <summary>
		/// Construct a new Scene
		/// </summary>
		public Scene()
		{
			Blobs		= new List<Blob>();
			Avatars		= new List<Avatar>();
			Thingies	= new List<Thingy>();

			// Mother nature takes care of how many thingies there are and balances the eco system
			nature = new Thread(new ThreadStart(MotherNature));
		}

		/// <summary>
		/// Create a copy of the list of avatars and return it
		/// </summary>
		public List<Avatar> GetAvatars(double x, double y, double range)
		{
			//lock (sceneLock)
			{
				if (x < 0 || y < 0 || x > Globals.ScreenWidth || y > Globals.ScreenHeight || range <= 0)
				{
					return new List<Avatar>(Avatars);
				}
				else
				{
					// list of avatars to return
					List<Avatar> inrange = new List<Avatar>();

					// The absolute X and Y coordinate
					int absX = Globals.ToAbsX(x);
					int absY = Globals.ToAbsY(y);

					//lock (sceneLock)
					{
						lock (Avatars)
						{
							foreach (Avatar a in Avatars)
							{
								// Calculate the distance to this avatar
								double distance = Math.Sqrt(Math.Pow(Math.Abs(absX - Globals.ToAbsX(a.X)), 2) + Math.Pow(Math.Abs(absY - Globals.ToAbsY(a.Y)), 2));
								if (distance <= range && distance > 100)
								{
									inrange.Add(a);
								}
							}
						}
					}

					return inrange;
				}
			}
		}

		/// <summary>
		/// Will be invoked by the event system when a blog has been updated according to ParleVision.
		/// </summary>
		/// <param name="sender">From where the event came</param>
		/// <param name="args">Arguments where we can extract info from</param>
		public void BlobChanged(object sender, Events.BlobChangedEventArgs args)
		{
			//lock (sceneLock)
			{
				Blob blob = null;

				List<Blob> candidates = new List<Blob>();
				lock (Blobs)
				{
					var selection = from b in Blobs
									where b.ID == args.ID
									select b;

					candidates.AddRange(selection);
				}

				// Have any candidates?
				if (candidates.Count == 1)
				{
					blob = candidates.First();
					blob.Move(args.X, args.Y, args.Width, args.Height);
				}
				// No candidates, it's a new blob!
				else
				{
					blob = new Blob(args.ID, args.X, args.Y, args.Width, args.Height);
					lock (Blobs) { Blobs.Add(blob); }

					// Creating an avatar requires a list of blobs
					List<Blob> ablobs = new List<Blob>();
					ablobs.Add(blob);

					// So we should also create an avatar for this blob the first time
					Avatar a = new Avatar(ablobs);
					lock (Avatars) { Avatars.Add(a); }
				}

				lock (Avatars)
				{
					foreach (Avatar a in Avatars)
					{
						if (!a.ContainsBlob(blob))
							continue;

						AvatarMoveMessage amm = new AvatarMoveMessage(a);
						MessageServer.Instance.Queue(amm);
					}
				}
			}

			//Debug.WriteLine(string.Format("BlobChanged invoked...! {0} {1} {2} {3} {4}", args.ID, args.X, args.Y, args.Width, args.Height));
		}

		/// <summary>
		/// Cretaceous-Tertiary Mass Extinction. The thing that killed the dinosaurs 65 million years ago.
		/// </summary>
		public void Extinct()
		{
			stop = true;

			// Wait for mother nature to stop
			nature.Join();
		}

		/// <summary>
		/// To be started up by a thread, this makes the scene alive!
		/// </summary>
		public void Live()
		{
			// Make sure mother nature is there to guard everyone
			nature.Start();

			// While we're still alive
			while (!stop)
			{
				//Debug.WriteLine("ClearOldBlobs");
				ClearOldBlobs();
				//Debug.WriteLine("DetermineMerges");
				DetermineMerges();
				//Debug.WriteLine("AvatarsEatThingies");
				AvatarsEatThingies();

				//lock (sceneLock)
				{
					try
					{
						//Debug.WriteLine("Checking invalid Thingies");
						// If there are thingies with an invalid location, remove them
						foreach (Thingy t in Thingies)
						{
							if (t.X < 0 || t.X > 1 || t.Y < 0 || t.Y > 1)
							{
								t.Die();
								Thingies.Remove(t);
							}
						}

						//Debug.WriteLine("Checking invalid Blobs");
						// Same goes for blobs
						lock (Blobs)
						{
							foreach (Blob b in Blobs)
							{
								if (b.X < 0 || b.X > Globals.ScreenWidth || b.Y < 0 || b.Y > Globals.ScreenHeight)
								{
									Blobs.Remove(b);
									break;
								}
							}
						}
					}
					catch (InvalidOperationException ioe)
					{
						// Oops, collection was modified
						Debug.WriteLine("EXCEPTION: Collection modified in Scene::Live()");
					}
				}

				//Debug.WriteLine("Sending active ID's");
				// Queue a message that contains all active avatar and thingy ID's
				ActiveIdsMessage aim = new ActiveIdsMessage(new List<Avatar>(Avatars), new List<Thingy>(Thingies));
				MessageServer.Instance.Queue(aim);

				// This is our FPS
				Thread.Sleep(1000 / 30);
			}
		}

		/// <summary>
		/// Takes care of the eco system
		/// </summary>
		private void MotherNature()
		{
			// Mother nature is always there
			while (!stop)
			{
				//lock (sceneLock)
				{
					// The number of thingies the eco system requires to properly function
					int thingyCount = Math.Min(4 + (Blobs.Count * 2), 12);
					if (Thingies.Count < thingyCount)
					{
						Thingy thingy = new Thingy(this);
						thingy.Birth();
						Thingies.Add(thingy);
					}

					//Debug.WriteLine(ToString());

					// Too much thingies, we need to take one away
					if (Thingies.Count > thingyCount)
					{
						Thingy victim = Thingies.First();
						victim.Die();
						Thingies.Remove(victim);
					}

					/*
					Console.WriteLine(Globals.Separator);
					Console.WriteLine("Blobs.Count = {0}", Blobs.Count);

					Console.WriteLine(Globals.Separator);
					Console.WriteLine("Avatars.Count = {0}", Avatars.Count);
					
					foreach (Avatar a in Avatars)
						Console.WriteLine(a.ToString());

					Console.WriteLine(Globals.Separator);
					Console.WriteLine("Thingies.Count = {0}", Thingies.Count);

					foreach (Thingy t in Thingies)
						Console.WriteLine(t.ToString());

					Console.WriteLine(Globals.Separator);
					*/
				}

				// Mother nature is a busy woman, let's not have her do too much. She's also old.
				Thread.Sleep(500);
			}

			// When we need to stop. It's a sad moment, but the thingies must be killed by mother.
			foreach (Thingy t in Thingies)
				t.Die();

			Debug.WriteLine("Mother nature says good bye. Be safe when I'm not here.");
		}

		/// <summary>
		/// To remove old blobs from the system
		/// </summary>
		private void ClearOldBlobs()
		{
			//lock (sceneLock)
			{
				List<Blob> oldBlobs = new List<Blob>();
				lock (Blobs)
				{
					// Blobs may change their ID's because ParleVision isn't able to track them properly. That's
					// why we need this to remove blobs that haven't been updated for a while.
					var selection = from b in Blobs
									where (DateTime.Now - b.LastUpdate).TotalSeconds > 1
									select b;

					oldBlobs.AddRange(selection);
				}
				lock (Avatars)
				{
					var selection = from a in Avatars
									from b in a.GetBlobs()
									where (DateTime.Now - b.LastUpdate).TotalSeconds > 1
									select b;

					oldBlobs.AddRange(selection);
				}

				if (oldBlobs.Count > 0)
				{
					// Now we have to find these blobs in the avatars
					foreach (Blob b in oldBlobs)
					{
						// Keep a list of avatars that should be removed
						List<Avatar> oldAvatars = new List<Avatar>();

						lock (Avatars)
						{
							// Now check all avatars for the presence of this blob
							foreach (Avatar a in Avatars)
							{
								if (a.ContainsBlob(b))
									a.RemoveBlob(b);

								if (a.GetBlobs().Count == 0)
									oldAvatars.Add(a);
							}

							// Remove old avatars
							foreach (Avatar oa in oldAvatars)
								Avatars.Remove(oa);
						}
					}

					if (Blobs.Contains(oldBlobs.First()))
					{
						lock (Blobs) { Blobs.Remove(oldBlobs.First()); }
					}
				}
			}
		}

		/// <summary>
		/// This checks all blobs and avatar combinations and checks whether they should merge or split
		/// </summary>
		private void DetermineMerges()
		{
			//lock (sceneLock)
			{
				bool shouldBreak = false;

				lock (Avatars)
				{
					// Walk through all blobs that are on the scene currently
					foreach (Avatar av1 in Avatars)
					{
						// Check if they intersect with each other
						foreach (Avatar av2 in Avatars)
						{
							// Don't do anything if they are the same
							if (av1 == av2)
								continue;

							// See if they should merge
							if (av1.GetBoundingRect().IntersectsWith(av2.GetBoundingRect()))
							{
								//Console.WriteLine("MERGE");

								// Remove the avatars from the scene!
								Avatars.Remove(av1);
								Avatars.Remove(av2);

								// Build a list of all blobs for the avatar
								List<Blob> ablobs = new List<Blob>();
								ablobs.AddRange(av1.GetBlobs());
								ablobs.AddRange(av2.GetBlobs());

								// Now, create a new avatar
								Avatar a = new Avatar(ablobs);
								Avatars.Add(a);

								// Fire message that it merged!
								AvatarMergeMessage msg = new AvatarMergeMessage(a, new List<Avatar>(new Avatar[] { av1, av2 }));
								MessageServer.Instance.Queue(msg);

								shouldBreak = true;
								break;
							}
						}

						if (shouldBreak)
							break;
					}
				} // lock avatars

				// Sometimes avatars should unmerge as well.. Check if the blobs in the current avatars are still okay
				shouldBreak = false;
				lock (Avatars)
				{
					foreach (Avatar a in Avatars)
					{
						// Avatars with only 1 blob should never unmerge
						if (a.GetBlobs().Count == 1)
							continue;

						// Check if a blob still belongs to the avatar
						foreach (Blob b in a.GetBlobs())
						{
							// If it doesn't intersect anymore..
							if (!b.BoundingRect.IntersectsWith(a.GetBoundingRect(b)))
							{
								//Console.WriteLine("UN_MERGE");

								// Then this blob should be removed from the avatar!
								a.RemoveBlob(b);

								List<Blob> ablobs = new List<Blob>();
								ablobs.Add(b);

								// Blob gets its own avatar again
								Avatar aNew = new Avatar(ablobs);
								Avatars.Add(aNew);

								// Fire message that we split!
								AvatarSplitMessage msg = new AvatarSplitMessage(aNew, a);
								MessageServer.Instance.Queue(msg);

								shouldBreak = true;
								break;
							}
						}

						if (shouldBreak)
							break;
					}
				} // lock avatars
			} //lock
		}

		/// <summary>
		/// Avatars w/ weight 1 can eat thingies. Avatars w/ a bigger weight attract thingies so can't eat 'em. When an avatar its bounding
		/// box intersects with the bounding box of a thingy, we say that the thingy is being eaten. A message should also be fired then.
		/// </summary>
		private void AvatarsEatThingies()
		{
			try
			{
				bool shouldBreak = false;

				List<Avatar> avatarsWithWeight1 = new List<Avatar>();
				lock (Avatars)
				{
					// Select all avatars w/ weight 1
					var selection = from a in Avatars
									where a.Weight == 1
									select a;

					avatarsWithWeight1.AddRange(selection);
				}

				// Now we must check whether an avatar is in range of a thingy!
				foreach (Avatar avatar in avatarsWithWeight1)
				{
					//lock (sceneLock)
					{
						// Check for each thingy
						foreach (Thingy thingy in Thingies)
						{
							if (avatar.GetBoundingRect().IntersectsWith(thingy.GetBoundingRect()))
							{
								// The thingy is eaten! Kill it and remove it from the system.
								thingy.Die();
								Thingies.Remove(thingy);

								// Fire a message that this happened
								ThingyEatenMessage tem = new ThingyEatenMessage(avatar, thingy);
								MessageServer.Instance.Queue(tem);

								// Then also break, because the collection is modified.
								shouldBreak = true;
							}

							if (shouldBreak)
								break;
						}
					}

					if (shouldBreak)
						break;
				}
			}
			catch (Exception) // Okay niet netjes, maar het werkt even...
			{
			}
		}

		public override string ToString()
		{
			return "Current scene contains " + Thingies.Count + " thingies, " + Blobs.Count + " blobs, " + Avatars.Count + " avatars";
		}
	}
}
